package org.algorithms.pingpong;

import java.awt.EventQueue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.UIManager;

import org.ActionConstants;
import org.ApplicationException;
import org.ConfigLoader;

public class PanelControllerImpl implements PanelController {
	private ProcessCi processCi;
	private DatagramSocket dataSocket;
	private ControlPanelImpl controlPanel;
	private Integer numberOfProcesses;
	private Integer tokenSpeed;
	private boolean mutexAcquired;
	private boolean mutexAcquiredByMe;
	private boolean trainMoving;
	private boolean iWantMutex;
	
	public PanelControllerImpl() {
		 setTokenSpeed(new Integer(ConfigLoader.configBundle.getString("token.speed")));
		 setNumberOfProcesses(new Integer(ConfigLoader.configBundle.getString("number.of.processes")));
	}
	
	/**
	 * Application Main
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
			JFrame.setDefaultLookAndFeelDecorated(true);
			JDialog.setDefaultLookAndFeelDecorated(true);
		} catch (Throwable e) {	}
		
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					ControlPanelImpl frame = new ControlPanelImpl();
					frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}
	
	@Override
	public void initialSetup(ProcessCi processCi) throws ApplicationException {
		this.processCi = processCi;
		try {
			createSocket();
		} catch (SocketException e) {
			throw new ApplicationException(e.getMessage());
		}
		setupRingTopology();
		pingPong();
	}

	private void setupRingTopology() {
		int nextProcNumber = getNumberOfNextProcess(getProcessCi().getNumber());
		int prevProcNumber = getNumberOfNextProcess( nextProcNumber );
		
		getProcessCi().getPrevProcessCi().setNumber(prevProcNumber);
		getProcessCi().getPrevProcessCi().setPort(getPortForProcess(prevProcNumber));
		
		getProcessCi().getNextProcessCi().setNumber(nextProcNumber);
		getProcessCi().getNextProcessCi().setPort(getPortForProcess(nextProcNumber));
	}

	private int getPortForProcess(int processNumber) {
		String strCiPort = "c" + processNumber + ".port";
		return new Integer(ConfigLoader.configBundle.getString(strCiPort)).intValue();
	}
	
	private int getNumberOfNextProcess(int currentProcessNumber) {
		return ( currentProcessNumber % getNumberOfProcesses()) + 1;
	}
	
	private void createSocket() throws SocketException {
		Integer portNumber = getPortForProcess(getProcessCi().getNumber());
		dataSocket = new DatagramSocket(portNumber, processCi.getIpAddress());
		getControlPanel().logMessage("C" + getProcessCi().getNumber() +" listening in Port:  " + dataSocket.getLocalPort(), false);
	}
	
	private void sendTokenToNextProcess(Integer token) {
		if (getControlPanel().interruptPing() && token > 0) return;
		if (getControlPanel().interruptPong() && token < 0) return;
		
		// sleep a while before sending, otherwise it is too fast
		int sleepTime = getTokenSpeed();
		try {
			Thread.sleep(sleepTime);
		} catch (InterruptedException e1) {	}
		
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(32);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(token.intValue());
		} catch (IOException e) { }
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();
		ProcessCi nextHost = processCi.getNextProcessCi();
		DatagramPacket dataPacket = new DatagramPacket(
				outputBuffer, 
				outputBuffer.length,
				nextHost.getIpAddress(),
				nextHost.getPort());
		try {
			dataSocket.send(dataPacket);
			logTokenDispatch(getProcessCi(), token);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * To be executed only by the last process of the ring
	 */
	private void startPingPong(int nbping, int nbpong) {
		sendTokenToNextProcess(nbping);
		sendTokenToNextProcess(nbpong);
	}
	
	/**
	 * Logs in ControlPanel that a token was dispatched
	 * @param processCi
	 * @param tokenValue
	 */
	private void logTokenDispatch(ProcessCi processCi, int tokenValue) {
		String tokenName = (tokenValue > 0) ? "nbping" : "nbpong";
		String msg = "[C"+getProcessCi().getNumber()+"] sent " + tokenName + ":" + tokenValue + " to: C" + getProcessCi().getNextProcessCi().getNumber() + ";";
		getControlPanel().logMessage( msg, true );
	}
	
	@Override
	public void sendActionToOthersCi(int actionNumber) {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(32);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(actionNumber);
		} catch (IOException e) { }
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();
		ProcessCi nextHost = processCi.getNextProcessCi();
		ProcessCi prevHost = processCi.getPrevProcessCi();
		DatagramPacket dataPacket;
		dataPacket = new DatagramPacket(
				outputBuffer, outputBuffer.length,
				nextHost.getIpAddress(), nextHost.getPort());
		try {
			dataSocket.send(dataPacket);
		} catch (IOException e) { }
		
		dataPacket = new DatagramPacket(
				outputBuffer, outputBuffer.length,
				prevHost.getIpAddress(), prevHost.getPort());
		try {
			dataSocket.send(dataPacket);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Send Messages to Train
	 * @param actionNumber
	 */
	private void sendActionToTrain(Integer actionNumber) {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(32);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		
		try {
			dataOutputStream.writeInt(actionNumber);
		} catch (IOException e) { }
		
		final byte[] outputBuffer = byteArrayOutputStream.toByteArray();
		int port = getDataSocket().getLocalPort() + getNumberOfProcesses();
		DatagramPacket dataPacket = new DatagramPacket(
				outputBuffer, 
				outputBuffer.length,
				getProcessCi().getIpTrain(), 
				port);
		try {
			dataSocket.send(dataPacket);
		} catch (IOException e) { }
	}
	
	private void pingPong() {
		Runnable runnable = new Runnable() {
			public void run() {
				final byte[] inputBuffer = new byte[32];
				final ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(inputBuffer);
				final DataInputStream dataInputStream = new DataInputStream(arrayInputStream);
				final DatagramPacket dataPacket = new DatagramPacket(inputBuffer, inputBuffer.length);
				
				int nbping = 1;
				int nbpong = -1;
				int token = 0;
				boolean pingHere = false;
				boolean pongHere = false;
				boolean regenerate = false;
				
				if (getProcessCi().getNumber() == getNumberOfProcesses().intValue()) {
					getControlPanel().getBtnInitControl().setEnabled(true);
					sendActionToOthersCi(ActionConstants.ENABLE_INIT_CONTROL);
					startPingPong(nbping, nbpong);
				}
				
				while (true) {
					try {
						dataSocket.receive(dataPacket);
					} catch (Throwable ex) { }
					
					arrayInputStream.reset();
					try {
						int t = dataInputStream.readInt();
						if (t > ActionConstants.ACTIONS_BASELINE) {
							handleReceivedAction(t);
						} else {
							// it is a token
							if (t > 0) {
								// ping
								pingHere = true;
								nbping = t;
								if (pongHere) {
									// ping and pong met!
									nbping = nbping + 1;
									nbpong = nbpong - 1;
									if (nbping > (getNumberOfProcesses() + 1)) {
										nbping = 1;
										nbpong = -1;
									}
								} else if (token == nbping) {
									// pong lost, regenerate
									nbping = nbping + 1;
									nbpong = -nbping;
									if (nbping > (getNumberOfProcesses() + 1)) {
										getControlPanel().logMessage("Pong Regenerated!!", true);
										nbping = 1;
										nbpong = -1;
									}
									regenerate = true;
									getControlPanel().logMessage("Pong perdido. Novo pong gerado... ", true);
								} else {
									token = nbping;
								}
								handleMutualExclusion();
								sendTokenToNextProcess(nbping);
								if (regenerate) {
									regenerate = false;
									sendTokenToNextProcess(nbpong);
								}
								pingHere = false;
							} else {
								// pong
								pongHere = true;
								nbpong = t;
								if (pingHere) {
									// ping and pong met!
									nbpong = nbpong - 1;
									nbping = nbping + 1;
									if (Math.abs(nbpong) > (getNumberOfProcesses() + 1)) {
										nbpong = -1;
										nbping = 1;
									}
								} else if (token == nbpong) {
									// ping was lost, regenerate
									nbpong = nbpong - 1;
									nbping = -nbpong;
									if (Math.abs(nbpong) > (getNumberOfProcesses() + 1)) {
										getControlPanel().logMessage("Pong Regenerated!!", true);
										nbpong = -1;
										nbping = 1;
									}
									regenerate = true;
									getControlPanel().logMessage("Ping perdido. Novo ping gerado... ", true);
								} else {
									token = nbpong;
								}
								sendTokenToNextProcess(nbpong);
								if (regenerate) {
									regenerate = false;
									sendTokenToNextProcess(nbping);
								}
								pongHere = false;
							}
						}
					} catch (IOException ex) {
						ex.printStackTrace();
					}
				}
			}
		};
		new Thread(runnable).start();
	}
	
	/**
	 * Handle Received Actions
	 * @param actionNumber
	 */
	private void handleReceivedAction(Integer actionNumber) {
		switch (actionNumber) {
		case ActionConstants.ENABLE_INIT_CONTROL :
			getControlPanel().getBtnInitControl().setEnabled(true);
			break;
		case ActionConstants.MUTEX_ACQUIRED :
			// someone got mutex
			setMutexAcquired(true);
			setMutexAcquiredByMe(false);
			
			getControlPanel().getBtnInitControl().setEnabled(false);
			getControlPanel().getBtnReleaseControl().setEnabled(false);
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(false);
			break;
		case ActionConstants.MUTEX_RELEASED :
			// someone released mutex
			setMutexAcquired(false);
			setMutexAcquiredByMe(false);
			
			getControlPanel().getBtnInitControl().setEnabled(true);
			getControlPanel().getBtnReleaseControl().setEnabled(false);
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(false);
			break;
		case ActionConstants.INIT_MOVEMENT:
			setTrainMoving(true);
			break;
		case ActionConstants.PAUSE_MOVEMENT:
			setTrainMoving(false);
			break;
		}
	}
	
	@Override
	public void handleControlPanelAction(Integer actionNumber) {
		switch (actionNumber) {
		case ActionConstants.INIT_CONTROL:
			// if I could click then nobody has mutex
			setMutexAcquiredByMe(true);
			setMutexAcquired(true);
			setiWantMutex(true);
			
			getControlPanel().getBtnInitControl().setEnabled(false);
			getControlPanel().getBtnReleaseControl().setEnabled(true);
			if (isTrainMoving()) {
				getControlPanel().getBtnPauseMove().setEnabled(true);
				getControlPanel().getBtnReinitMove().setEnabled(false);
			} else {
				getControlPanel().getBtnReinitMove().setEnabled(true);
				getControlPanel().getBtnPauseMove().setEnabled(false);
			} 
			sendActionToOthersCi(ActionConstants.MUTEX_ACQUIRED);
			break;
		case ActionConstants.RELEASE_CONTROL :
			setMutexAcquired(false);
			setMutexAcquiredByMe(false);
			setiWantMutex(false);
			
			getControlPanel().getBtnInitControl().setEnabled(true);
			getControlPanel().getBtnReleaseControl().setEnabled(false);
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(false);
			
			sendActionToOthersCi(ActionConstants.MUTEX_RELEASED);
			break;
		case ActionConstants.INIT_MOVEMENT :
			setTrainMoving(true);
			sendActionToTrain(ActionConstants.INIT_MOVEMENT);
			
			sendActionToOthersCi(ActionConstants.INIT_MOVEMENT);
			break;
		case ActionConstants.PAUSE_MOVEMENT :
			setTrainMoving(false);
			sendActionToTrain(ActionConstants.PAUSE_MOVEMENT);
			
			sendActionToOthersCi(ActionConstants.PAUSE_MOVEMENT);
			break;
		}
	}
	
	/**
	 * Configurations for mutual exclusion acquired
	 */
	private void handleMutualExclusion() {
		if (iWantMutex()) {
			setMutexAcquired(true);
			setMutexAcquiredByMe(true);
			getControlPanel().getBtnReleaseControl().setEnabled(true);
			
			while (isMutexAcquiredByMe()) {
				if (isTrainMoving()) {
					getControlPanel().getBtnPauseMove().setEnabled(true);
					getControlPanel().getBtnReinitMove().setEnabled(false);
				} else {
					getControlPanel().getBtnReinitMove().setEnabled(true);
					getControlPanel().getBtnPauseMove().setEnabled(false);
				}
			}
			getControlPanel().getBtnReleaseControl().setEnabled(false);
			getControlPanel().getBtnPauseMove().setEnabled(false);
			getControlPanel().getBtnReinitMove().setEnabled(false);
		}
	}
	
	
	/*
	 * GETTERS AND SETTERS 
	 */

	@Override
	public Integer getNumberOfProcesses() {
		return numberOfProcesses;
	}
	
	@Override
	public void setNumberOfProcesses(Integer numberOfProcesses) {
		this.numberOfProcesses = numberOfProcesses;
	}
	
	@Override
	public ProcessCi getProcessCi() {
		return processCi;
	}

	@Override
	public void setProcessCi(ProcessCi processCi) {
		this.processCi = processCi;
	}

	@Override
	public ControlPanelImpl getControlPanel() {
		return controlPanel;
	}

	@Override
	public void setControlPanel(ControlPanelImpl controlPanel) {
		this.controlPanel = controlPanel;
	}

	public DatagramSocket getDataSocket() {
		return dataSocket;
	}

	public void setDataSocket(DatagramSocket dataSocket) {
		this.dataSocket = dataSocket;
	}

	public boolean isMutexAcquired() {
		return mutexAcquired;
	}

	synchronized public void setMutexAcquired(boolean mutexAcquired) {
		this.mutexAcquired = mutexAcquired;
	}

	public boolean isMutexAcquiredByMe() {
		return mutexAcquiredByMe;
	}

	synchronized public void setMutexAcquiredByMe(boolean mutexAcquiredByMe) {
		this.mutexAcquiredByMe = mutexAcquiredByMe;
	}

	public boolean isTrainMoving() {
		return trainMoving;
	}

	synchronized public void setTrainMoving(boolean trainMoving) {
		this.trainMoving = trainMoving;
	}

	public boolean iWantMutex() {
		return iWantMutex;
	}

	synchronized public void setiWantMutex(boolean iWantMutex) {
		this.iWantMutex = iWantMutex;
	}

	private Integer getTokenSpeed() {
		return tokenSpeed;
	}

	private void setTokenSpeed(Integer tokenSpeed) {
		this.tokenSpeed = tokenSpeed;
	}
}